package aspectjutil.adviseutil;

import java.util.ArrayList;
import java.util.List;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Util {
	protected static Logger logger = LoggerFactory.getLogger( Util.class);
	
	/**
	 * @param stackTraceElementArray
	 * @param from : Usually needs to skip first 5 elements.
	 * 	First 2 elements are about trace of method being called.
	 * 	Next 3 elements are by aspectj's advise method.
	 * @param count : Number of elements to pick up after skipping the number of elements specified by "from" argument.
	 * @return
	 */
	
	public static List<String> getCallingMethodInfo( 
			final StackTraceElement[] stackTraceElementArray, int from, int count) 
	{
		List<String> stackTraceElementStringList = new ArrayList<String>();
		
		if ( from < 0) return stackTraceElementStringList;
		if ( count < 1) return stackTraceElementStringList;
		
		for( int stackTraceElementIndex = from; stackTraceElementIndex < stackTraceElementArray.length; stackTraceElementIndex++) {
			String className = stackTraceElementArray[ stackTraceElementIndex].getClassName();
			if ( className.startsWith( "sun.reflect") || className.startsWith( "java.lang.reflect"))
			{
				continue;
			}
			// Store 2 elements about call before reflective invocation in stackTraceElementArray into stackTraceElementStringBuilder 
			String message 
			= String.format(
					"StackTraceElement[ %1$d]: %2$s%n",
					stackTraceElementIndex,
					stackTraceElementArray[ stackTraceElementIndex].toString()
					);
			stackTraceElementStringList.add( message);
			if ( stackTraceElementStringList.size() >= count) break; // for
		} // for
		
		return stackTraceElementStringList;
	} // public static List<String> getCallingMethodInfo( final StackTraceElement[] stackTraceElementArray, int from, int count) 
	
	
	public static List<String> getCallingMethodInfo( 
			final StackTraceElement[] stackTraceElementArray, final JoinPoint joinPoint, int count) 
	{
		List<String> stackTraceElementStringList = new ArrayList<String>();
		
		// arguments validation -------------------------------------------------------------------
		if ( stackTraceElementArray == null) {
			throw new IllegalArgumentException( "stackTraceElementArray argument cannot be null");
		}
		if ( joinPoint == null) {
			throw new IllegalArgumentException( "joinPoint argument cannot be null");
		}
		if ( count < 1) {
			throw new IllegalArgumentException( "value of count argument cannot be smaller than 1");
		}
		
/*		if ( !joinPoint.getKind().startsWith( "method-") && !joinPoint.getKind().startsWith( "constructor-")) {
			return stackTraceElementStringList;
		}
*/
		if ( 
				!joinPoint.getKind().equals( JoinPoint.METHOD_CALL)
				&& !joinPoint.getKind().equals( JoinPoint.METHOD_EXECUTION)
				&& !joinPoint.getKind().equals( JoinPoint.CONSTRUCTOR_CALL)
				&& !joinPoint.getKind().equals( JoinPoint.CONSTRUCTOR_EXECUTION)
				) 
		{
			return stackTraceElementStringList;
		}
		// ----------------------------------------------------------------------------------------
		
		String methodName = joinPoint.getSignature().getName();
		int indexAtTarget = stackTraceElementArray.length;
		boolean indexAtTargetSetFlag = false;
		
		String joinPointClassName;
		
		// By call pointcuts on constructor, joinPoint.getTarget() is null
		if ( joinPoint.getTarget() != null) {
			joinPointClassName = joinPoint.getTarget().getClass().getName();
			
			for( int stackIndex = 2; stackIndex < stackTraceElementArray.length; stackIndex++) {
				/* Skip first 2 elements in stackTraceElementArray because:
				 * 	first element is from getStackTrace execution
				 * 	next elements is about trace of calling to method in where stacktrace was captured
				 */
				
				String className = stackTraceElementArray[ stackIndex].getClassName();
				if ( 
						joinPointClassName.equals( className) 
						&& methodName.equals( stackTraceElementArray[ stackIndex].getMethodName())
						)
				{
					/* logic reaches here when one among following cases 
					 * 	- before-advise on exectuion pointcuts case
					 * 	- after-advise on exectuion pointcuts case
					 * 	- around-advise on exectuion pointcuts case
					 * Interesting thing is that joinPoint.getTarget is in stacktrace even either cases of 
					 * getting stacktrace before or after invoking proceedingJoinPoint.proceed(). 
					 */
					if ( !indexAtTargetSetFlag) {
						indexAtTarget = stackIndex;
						indexAtTargetSetFlag = true;
						continue;
					}
				}
				if ( stackIndex > indexAtTarget) {
					if ( className.startsWith( "sun.reflect") || className.startsWith( "java.lang.reflect"))
					{
						continue;
					}
					// Store stacked invocation info before reflective invocation in stackTraceElementArray into stackTraceElementStringBuilder 
					String message 
					= String.format(
							"StackTraceElement[ %1$d]: %2$s%n",
							stackIndex,
							stackTraceElementArray[ stackIndex].toString()
							);
					stackTraceElementStringList.add( message);
					if ( stackTraceElementStringList.size() >= count) break; // for
				}
			} // for
		}
		
		if ( !indexAtTargetSetFlag) { // Target of pointcuts is not in stacktrace
			/* When logic reaches here, it's one among following cases:
			 * 	- before-advise on call pointcuts
			 * 	- after-advise on call pointcuts
			 * 	- around-advise on call pointcuts
			 * and joinPoint.getTarget won't be in stacktrace.
			 */
			if ( joinPoint.getThis() != null) { 
				joinPointClassName = joinPoint.getThis().getClass().getName();

				for( int stackIndex = 2; stackIndex < stackTraceElementArray.length; stackIndex++) {
					/* Skip first 2 elements in stackTraceElementArray because:
					 * 	first element is from getStackTrace execution
					 *  next elements is, in shortest path, about trace of advise method. 
					 */
					
					String className = stackTraceElementArray[ stackIndex].getClassName();
					if ( joinPointClassName.equals( className))
					{
						if ( !indexAtTargetSetFlag) {
							indexAtTarget = stackIndex;
							indexAtTargetSetFlag = true;
							
							String message = String.format(
									"Guessed pick-up at StackTraceElement[ %1$d]: %2$s%n",
									stackIndex,
									stackTraceElementArray[ stackIndex].toString()
									);
							
							stackTraceElementStringList.add( message);
							if ( stackTraceElementStringList.size() >= count) break; // for
							continue;
						}
					}
					if ( stackIndex > indexAtTarget) {
						if ( className.startsWith( "sun.reflect") || className.startsWith( "java.lang.reflect"))
						{
							continue;
						}
						// Store stacked invocation info before reflective invocation in stackTraceElementArray into stackTraceElementStringBuilder 
						String message 
						= String.format(
								"Guessed pick-up at StackTraceElement[ %1$d]: %2$s%n",
								stackIndex,
								stackTraceElementArray[ stackIndex].toString()
								);
						stackTraceElementStringList.add( message);
						if ( stackTraceElementStringList.size() >= count) break; // for
					}
				} // for
			}
			else {
				// this-object might have been destroyed if another same class with one being advised in class path 
				if ( logger.isWarnEnabled()) {
					logger.warn(
							String.format(
									"Null was returned by getThis method of JoinPoint on %1$s.%n" 
									+ "It may be just because the join point was on object with static modifier. " 
									+ "Or it may be caused by the existence of another same class not " 
									+ "advised in the class path. Please check if suspicious on later case.",
									joinPoint.toString()
									)
							);
				}
			}
		}
		
		return stackTraceElementStringList;
	} // public static List<String> getCallingMethodInfo( final StackTraceElement[] stackTraceElementArray, JoinPoint joinPoint, int count)
	
	public static String getJoinPointInfo( 
			final JoinPoint joinPoint, final JoinPoint.EnclosingStaticPart enclosingStaticPart) {
		
		// Dump arguments -------------------------------------------------------------------------
		Object[] argumentsObjArray = joinPoint.getArgs();
		
		StringBuilder argumentsStringBuilder = new StringBuilder();
		for( int argIndex = 0; argIndex < argumentsObjArray.length; argIndex++) {
			if ( argIndex > 0) {
				argumentsStringBuilder.append( ", ");
			}
			argumentsStringBuilder.append( argumentsObjArray[ argIndex]);
		} // for
		// ----------------------------------------------------------------------------------------
		
		String joinPointSignatureStr = joinPoint.getSignature().toString();
		
		String joinPointInfo 
		= String.format(
				"Type: %1$s%n" +
				"Signature: %2$s%n" +
				"Arguments: ( %3$s )",
				( joinPoint.getTarget() == null ? "no type info" : joinPoint.getTarget().getClass().getName()),
				joinPointSignatureStr,
				argumentsStringBuilder
				);
		
		// Add calling method infomration ---------------------------------------------------------
		if (  
				enclosingStaticPart != null 
				&& !joinPoint.getSourceLocation().toString().equals( enclosingStaticPart.getSourceLocation().toString())
				) 
		{ // Use enclosingStaticPart to get calling method infomration
			joinPointInfo = String.format(
					"%1$s%n" +
					"Called from %2$s (%3$s)",
					joinPointInfo, 
					((CodeSignature)enclosingStaticPart.getSignature()).toString(),
					enclosingStaticPart.getSourceLocation().toString()
					);
		}
		else { // Use stack-trace to get calling method infomration
			String callingMethodInfo = "";
			StackTraceElement[] stackTraceElementArray = Thread.currentThread().getStackTrace();
			
			for( String stackTraceElementString : getCallingMethodInfo( stackTraceElementArray, joinPoint, 2)) {
				callingMethodInfo 
				= String.format( 
						"%1$s" +
						"%2$c%3$s",
						callingMethodInfo, 
						'\t', 
						stackTraceElementString
						);
			} // for
			
			joinPointInfo = String.format(
					"%1$s%n" +
					"Called from:%n %2$s",
					joinPointInfo, 
					callingMethodInfo
					);
		}
		// ----------------------------------------------------------------------------------------
		
		return joinPointInfo;
	} // public static String getJoinPointInfo( JoinPoint joinPoint, JoinPoint.EnclosingStaticPart enclosingStaticPart)
}
